package model.core;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;


/**
 * Regarding the use of this class: <br>
 * cycle: one 'loop' ~= 30ms<br>
 * period: the number of cycles between a Notifiable's execution<br>
 * lifetime: the total number of cycles a Notifiable will remain in the Scheduler<br>
 * @author Paul
 *
 */

public class GameScheduler {
	private Timer timer = new Timer();
	
	private static GameScheduler scheduler = null;
	
	private HashMap<Notifiable, TimedObject> scheduled = new HashMap<Notifiable, TimedObject>();
	private HashMap<Notifiable, LifeTimedObject> scheduledWithLifetime = new HashMap<Notifiable, LifeTimedObject>();
	private HashSet<Notifiable> scheduledOnce = new HashSet<Notifiable>();
	private HashMap<Notifiable, TimedObject> toRegister = new HashMap<Notifiable, TimedObject>();
	private ArrayList<Notifiable> toRemove = new ArrayList<Notifiable>();
	
	private static Object lock = new Object();
	boolean running = false;
	
	/**
	 * GameScheduler functions as a singleton: access it through the getInstance() method!
	 */
	private GameScheduler(){
		timer.scheduleAtFixedRate(tick(), 30, 30);
		this.running = true;
	}
	
	/**
	 * Use this to access the single instance of GameScheduler
	 * @return scheduler the one instance of GameScheduler
	 */
	public synchronized static GameScheduler getInstance(){
		synchronized(lock){
			if (scheduler == null){
				scheduler = new GameScheduler();
			}
			return scheduler;
		}
	}
	
	/**
	 * Registers a Notifiable object with the Scheduler.
	 * @param timeInCycles the period (in 30ms cycles) of this scheduled event.
	 * @param toSchedule the event to be scheduled.
	 */
	public synchronized void register(int timeInCycles, Notifiable toSchedule){
		synchronized(lock){
			toRegister.put(toSchedule, new TimedObject(timeInCycles));
		}
	}
	
	/**
	 * Registers a Notifiable object with the Scheduler for a lifetime.
	 * @param period the period of this scheduled event.
	 * @param lifetime the total time this event will exist.
	 * @param toSchedule the event to be scheduled.
	 */
	public synchronized void timedRegister(int period, int lifetime, Notifiable toSchedule){
		synchronized(lock){
			scheduledWithLifetime.put(toSchedule, new LifeTimedObject(period, lifetime));
		}
	}
	
	/**
	 * Registers a Notifiable object for a single execution.
	 * @param toSchedule the event to be scheduled.
	 * @param delay the delay (in cycles) before execution.
	 */
	public synchronized void registerOnce(Notifiable toSchedule, int delay){
		synchronized (toSchedule) {
			scheduled.put(toSchedule, new TimedObject(delay));
			scheduledOnce.add(toSchedule);
		}
	}
	
	/**
	 * Removes the given Notifiable object from the Scheduler AFTER the next cycle.
	 * @param remove the object to be removed.
	 */
	public synchronized void unRegister (Notifiable remove){
		synchronized(lock){
			System.out.println("REMOVING");
			toRemove.add(remove);
		}
	}
	
	/**
	 * Defines what will happen on every tick (cycle).  
	 * If the current cycle ends the period of a Scheduled object, 
	 * that Notifiable's <code>execute()</code> method is called.
	 * <br><br>
	 * If a Notifiable is scheduled to be removed, it is executed, THEN removed.
	 * @return
	 */
	private TimerTask tick(){
		return new TimerTask() {
			public void run() {
				if(running){
					//System.out.println("Running!");
					synchronized(lock) {
						for (Notifiable n : scheduled.keySet()) {
							if (scheduled.get(n).tick()) {
								n.execute();
							}
						}
						for (Notifiable n : scheduledWithLifetime.keySet()) {
							if (scheduledWithLifetime.get(n).tick()) {
								n.execute();
							}
							else if (scheduledWithLifetime.get(n).removeCheck()){
								toRemove.add(n);
							}
						}
						for (Notifiable n : scheduledOnce) {
							if (scheduled.get(n).tick()){
								n.execute();
								scheduledOnce.remove(n);
								unRegister(n);
							}
						}
						for (Notifiable n : toRemove) {
							if (scheduled.containsKey(n)){
								scheduled.remove(n);
							}
						}
						for(Notifiable n : toRegister.keySet())
						{
							scheduled.put(n, toRegister.get(n));
						}
						toRegister = new HashMap<Notifiable, TimedObject>();
						toRemove = new ArrayList<Notifiable>();
					}
				} else {
					this.cancel();
				}
				
			}
		};
	}
	
	/**
	 * Starts the timer (either the first time OR after a pause).
	 */
	public synchronized void startTimer(){
		running = false;
		timer = new Timer();
		timer.scheduleAtFixedRate(tick(), 30, 30);
	}
	
	/**
	 * Stops the timer (can be used as pause).
	 */
	public synchronized void stopTimer(){
		System.out.println("STOPPING!");
		running = false;
		timer.cancel();
	}
	
	
	private class TimedObject{
		private int period;
		private int counter;
		
		TimedObject(int p){
			this.period = p;
			this.counter = 0;
		}
		boolean tick(){
			return ((counter++ % period) == 0);
		}
	}
	
	private class LifeTimedObject extends TimedObject{
		private int lifetime;
		private boolean removeMe;
		LifeTimedObject(int p, int l) {
			super(p);
			this.lifetime = l;
			removeMe = false;
		}
		boolean tick(){
			if (--lifetime > 0){
				return super.tick();
			}
			else {
				removeMe = true;
				return false;
			}
		}
		boolean removeCheck(){
			return removeMe;
		}
	}
	
	/*
	private class TimerLoop extends Thread{
		private boolean isRunning = false;
		private GameScheduler scheduler = null;
		
		TimerLoop(GameScheduler g){
			this.scheduler = g;
			this.start();
		}
		
		public synchronized void run(){
			synchronized(lock){
				isRunning = true;
				while(true){
					if (isRunning){
						System.out.println("Running");
						scheduler.tick();
						try{
							Thread.sleep(30);
						}
						catch(Exception e){
					
						}
					}
					else{
						System.out.println("Not Running");
					}
				}
			}
		}
		synchronized void pause(){
			synchronized (lock) {
				System.out.println("PAUSED");
				isRunning = false;
			}
		}
		synchronized void unPause(){
			synchronized (lock) {
				System.out.println("STARTING");
				isRunning = true;
			}
		}
	}
	*/
}
